/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.schema;

import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.apache.lucene.index.IndexableField;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.util.RTimer;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

/** Tests CurrencyField and CurrencyFieldType. */
public class CurrencyFieldTypeTest extends SolrTestCaseJ4 {
  private final String fieldName;
  private final Class<? extends ExchangeRateProvider> expectedProviderClass;

  public CurrencyFieldTypeTest(
      String fieldName, Class<? extends ExchangeRateProvider> expectedProviderClass) {
    this.fieldName = fieldName;
    this.expectedProviderClass = expectedProviderClass;
  }

  @ParametersFactory
  public static Iterable<Object[]> parameters() {
    return Arrays.asList(
        new Object[][] {
          {"amount", FileExchangeRateProvider.class}, // CurrencyFieldType
          {"mock_amount", MockExchangeRateProvider.class}, // CurrencyFieldType
          {"oer_amount", OpenExchangeRatesOrgProvider.class} // CurrencyFieldType
        });
  }

  /** "Assumes" that the specified list of currency codes are supported in this JVM */
  public static void assumeCurrencySupport(String... codes) {
    try {
      // each JDK might have a diff list of supported currencies,
      // these are the ones needed for this test to work.
      for (String code : codes) {
        Currency obj = Currency.getInstance(code);
        assertNotNull(code, obj);
      }
    } catch (IllegalArgumentException e) {
      Assume.assumeNoException(e);
    }
  }

  @BeforeClass
  public static void beforeClass() throws Exception {
    assumeCurrencySupport("USD", "EUR", "MXN", "GBP", "JPY", "NOK");
    initCore("solrconfig.xml", "schema.xml");
  }

  @Test
  public void testCurrencySchema() {
    IndexSchema schema = h.getCore().getLatestSchema();

    SchemaField amount = schema.getField(fieldName);
    assertNotNull(amount);
    assertTrue(amount.isPolyField());

    CurrencyFieldType type = (CurrencyFieldType) amount.getType();
    String currencyDynamicField = "*" + type.fieldSuffixCurrency;
    String amountDynamicField = "*" + type.fieldSuffixAmountRaw;

    SchemaField[] dynFields = schema.getDynamicFieldPrototypes();
    boolean seenCurrency = false;
    boolean seenAmount = false;

    for (SchemaField dynField : dynFields) {
      if (dynField.getName().equals(amountDynamicField)) {
        seenAmount = true;
      }

      if (dynField.getName().equals(currencyDynamicField)) {
        seenCurrency = true;
      }
    }

    assertTrue(
        "Didn't find the expected currency code dynamic field " + currencyDynamicField,
        seenCurrency);
    assertTrue("Didn't find the expected value dynamic field " + amountDynamicField, seenAmount);
  }

  @Test
  public void testCurrencyFieldType() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    SolrCore core = h.getCore();
    IndexSchema schema = core.getLatestSchema();
    SchemaField amount = schema.getField(fieldName);
    assertNotNull(amount);
    assertTrue(fieldName + " is not a poly field", amount.isPolyField());
    FieldType tmp = amount.getType();
    assertTrue(
        fieldName + " is not an instance of CurrencyFieldType", tmp instanceof CurrencyFieldType);
    String currencyValue = "1.50,EUR";
    List<IndexableField> fields = amount.createFields(currencyValue);
    assertEquals(fields.size(), 3);

    // First field is currency code, second is value, third is stored.
    for (int i = 0; i < 3; i++) {
      boolean hasValue =
          fields.get(i).readerValue() != null
              || fields.get(i).numericValue() != null
              || fields.get(i).stringValue() != null;
      assertTrue("Doesn't have a value: " + fields.get(i), hasValue);
    }

    assertEquals(schema.getFieldTypeByName("string").toExternal(fields.get(2)), "1.50,EUR");

    // A few tests on the provider directly
    ExchangeRateProvider p = ((CurrencyFieldType) tmp).getProvider();
    Set<String> availableCurrencies = p.listAvailableCurrencies();
    assertEquals(5, availableCurrencies.size());
    assertTrue(p.reload());
    assertEquals(2.5, p.getExchangeRate("USD", "EUR"), 0.00000000001);
  }

  @Test
  public void testMockExchangeRateProvider() {
    assumeTrue(
        "This test is only applicable to the mock exchange rate provider",
        expectedProviderClass.equals(MockExchangeRateProvider.class));

    SolrCore core = h.getCore();
    IndexSchema schema = core.getLatestSchema();
    SchemaField field = schema.getField(fieldName);
    FieldType fieldType = field.getType();
    ExchangeRateProvider provider = ((CurrencyFieldType) fieldType).getProvider();

    // A few tests on the provider directly
    assertEquals(3, provider.listAvailableCurrencies().size());
    assertTrue(provider.reload());
    assertEquals(0.8, provider.getExchangeRate("USD", "EUR"), 0.00000000001);
  }

  @Test
  public void testCurrencyRangeSearch() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    clearIndex();
    final int emptyDocs = atLeast(50); // times 2
    final int negDocs = atLeast(5);

    assertU(adoc("id", "0", fieldName, "0,USD")); // 0
    // lots of docs w/o values
    for (int i = 100; i <= 100 + emptyDocs; i++) {
      assertU(adoc("id", "" + i));
    }
    // docs with values in ranges we'll query
    for (int i = 1; i <= 10; i++) {
      assertU(adoc("id", "" + i, fieldName, i + ",USD"));
    }
    // more docs w/o values
    for (int i = 500; i <= 500 + emptyDocs; i++) {
      assertU(adoc("id", "" + i));
    }
    // some negative values
    for (int i = -100; i > -100 - negDocs; i--) {
      assertU(adoc("id", "" + i, fieldName, i + ",USD"));
    }
    assertU(adoc("id", "40", fieldName, "0,USD")); // 0

    assertU(commit());

    assertQ(req("fl", "*,score", "q", fieldName + ":[2.00,USD TO 5.00,USD]"), "//*[@numFound='4']");

    assertQ(req("fl", "*,score", "q", fieldName + ":[0.50,USD TO 1.00,USD]"), "//*[@numFound='1']");

    assertQ(
        req("fl", "*,score", "q", fieldName + ":[24.00,USD TO 25.00,USD]"), "//*[@numFound='0']");

    // "GBP" currency code is 1/2 of a USD dollar, for testing.
    assertQ(req("fl", "*,score", "q", fieldName + ":[0.50,GBP TO 1.00,GBP]"), "//*[@numFound='2']");

    // "EUR" currency code is 2.5X of a USD dollar, for testing.
    assertQ(
        req("fl", "*,score", "q", fieldName + ":[24.00,EUR TO 25.00,EUR]"), "//*[@numFound='1']");

    // Slight asymmetric rate should work.
    assertQ(
        req("fl", "*,score", "q", fieldName + ":[24.99,EUR TO 25.01,EUR]"), "//*[@numFound='1']");

    // Open-ended ranges without currency
    assertQ(
        req("fl", "*,score", "q", fieldName + ":[* TO *]"),
        "//*[@numFound='" + (2 + 10 + negDocs) + "']");

    // Open-ended ranges without currency
    assertQ(
        req("fl", "*,score", "q", fieldName + ":*"), "//*[@numFound='" + (2 + 10 + negDocs) + "']");

    // Open-ended ranges with currency
    assertQ(
        req("fl", "*,score", "q", fieldName + ":[*,EUR TO *,EUR]"),
        "//*[@numFound='" + (2 + 10 + negDocs) + "']");

    // Open-ended start range without currency
    assertQ(
        req("fl", "*,score", "q", fieldName + ":[* TO 5,USD]"),
        "//*[@numFound='" + (2 + 5 + negDocs) + "']");

    // Open-ended start range with currency (currency for the * won't matter)
    assertQ(
        req("fl", "*,score", "q", fieldName + ":[*,USD TO 5,USD]"),
        "//*[@numFound='" + (2 + 5 + negDocs) + "']");

    // Open-ended end range
    assertQ(req("fl", "*,score", "q", fieldName + ":[3 TO *]"), "//*[@numFound='8']");
  }

  @Test
  public void testBogusCurrency() {
    ignoreException("HOSS");

    // bogus currency
    assertQEx(
        "Expected exception for invalid currency",
        req("fl", "*,score", "q", fieldName + ":[3,HOSS TO *]"),
        400);
  }

  @Test
  public void testCurrencyPointQuery() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    clearIndex();
    assertU(adoc("id", "" + 1, fieldName, "10.00,USD"));
    assertU(adoc("id", "" + 2, fieldName, "15.00,MXN"));
    assertU(commit());
    assertQ(req("fl", "*,score", "q", fieldName + ":10.00,USD"), "//str[@name='id']='1'");
    assertQ(req("fl", "*,score", "q", fieldName + ":9.99,USD"), "//*[@numFound='0']");
    assertQ(req("fl", "*,score", "q", fieldName + ":10.01,USD"), "//*[@numFound='0']");
    assertQ(req("fl", "*,score", "q", fieldName + ":15.00,MXN"), "//str[@name='id']='2'");
    assertQ(req("fl", "*,score", "q", fieldName + ":7.50,USD"), "//str[@name='id']='2'");
    assertQ(req("fl", "*,score", "q", fieldName + ":7.49,USD"), "//*[@numFound='0']");
    assertQ(req("fl", "*,score", "q", fieldName + ":7.51,USD"), "//*[@numFound='0']");
  }

  @Ignore
  public void testPerformance() {
    clearIndex();

    Random r = random();
    int initDocs = 200000;

    for (int i = 1; i <= initDocs; i++) {
      assertU(adoc("id", "" + i, fieldName, (r.nextInt(10) + 1.00) + ",USD"));
      if (i % 1000 == 0) System.out.println(i);
    }

    assertU(commit());
    for (int i = 0; i < 1000; i++) {
      double lower = r.nextInt(10) + 1.00;
      assertQ(
          req(
              "fl",
              "*,score",
              "q",
              fieldName + ":[" + lower + ",USD TO " + (lower + 10.00) + ",USD]"),
          "//*");
      assertQ(
          req(
              "fl",
              "*,score",
              "q",
              fieldName + ":[" + lower + ",EUR TO " + (lower + 10.00) + ",EUR]"),
          "//*");
    }

    for (int j = 0; j < 3; j++) {
      final RTimer timer = new RTimer();
      for (int i = 0; i < 1000; i++) {
        double lower = r.nextInt(10) + 1.00;
        assertQ(
            req(
                "fl",
                "*,score",
                "q",
                fieldName + ":[" + lower + ",USD TO " + (lower + (9.99 - (j * 0.01))) + ",USD]"),
            "//*");
      }

      System.out.println(timer.getTime());
    }

    System.out.println("---");

    for (int j = 0; j < 3; j++) {
      final RTimer timer = new RTimer();
      for (int i = 0; i < 1000; i++) {
        double lower = r.nextInt(10) + 1.00;
        assertQ(
            req(
                "fl",
                "*,score",
                "q",
                fieldName + ":[" + lower + ",EUR TO " + (lower + (9.99 - (j * 0.01))) + ",EUR]"),
            "//*");
      }

      System.out.println(timer.getTime());
    }
  }

  @Test
  public void testCurrencySort() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    clearIndex();

    assertU(adoc("id", "" + 1, fieldName, "10.00,USD"));
    assertU(adoc("id", "" + 2, fieldName, "15.00,EUR"));
    assertU(adoc("id", "" + 3, fieldName, "7.00,EUR"));
    assertU(adoc("id", "" + 4, fieldName, "6.00,GBP"));
    assertU(adoc("id", "" + 5, fieldName, "2.00,GBP"));
    assertU(commit());

    assertQ(
        req("fl", "*,score", "q", "*:*", "sort", fieldName + " desc", "limit", "1"),
        "//str[@name='id']='4'");
    assertQ(
        req("fl", "*,score", "q", "*:*", "sort", fieldName + " asc", "limit", "1"),
        "//str[@name='id']='3'");
  }

  public void testExpectedProvider() {
    SolrCore core = h.getCore();
    IndexSchema schema = core.getLatestSchema();
    SchemaField field = schema.getField(fieldName);
    FieldType fieldType = field.getType();
    ExchangeRateProvider provider = ((CurrencyFieldType) fieldType).getProvider();
    assertEquals(expectedProviderClass, provider.getClass());
  }

  public void testFunctionUsage() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    clearIndex();
    for (int i = 1; i <= 8; i++) {
      // "GBP" currency code is 1/2 of a USD dollar, for testing.
      assertU(adoc("id", "" + i, fieldName, (((float) i) / 2) + ",GBP"));
    }
    for (int i = 9; i <= 11; i++) {
      assertU(adoc("id", "" + i, fieldName, i + ",USD"));
    }

    assertU(commit());

    // direct value source usage, gets "raw" form of default currency
    // default==USD, so raw==pennies
    assertQ(
        req(
            "fl", "id,func:field($f)",
            "f", fieldName,
            "q", "id:5"),
        "//*[@numFound='1']",
        "//doc/float[@name='func' and .=500]");
    assertQ(
        req(
            "fl", "id,func:field($f)",
            "f", fieldName,
            "q", "id:10"),
        "//*[@numFound='1']",
        "//doc/float[@name='func' and .=1000]");
    assertQ(
        req(
            "fl", "id,score," + fieldName,
            "q", "{!frange u=500}" + fieldName),
        "//*[@numFound='5']",
        "//str[@name='id']='1'",
        "//str[@name='id']='2'",
        "//str[@name='id']='3'",
        "//str[@name='id']='4'",
        "//str[@name='id']='5'");
    assertQ(
        req(
            "fl", "id,score," + fieldName,
            "q", "{!frange l=500 u=1000}" + fieldName),
        "//*[@numFound='6']",
        "//str[@name='id']='5'",
        "//str[@name='id']='6'",
        "//str[@name='id']='7'",
        "//str[@name='id']='8'",
        "//str[@name='id']='9'",
        "//str[@name='id']='10'");

    // use the currency function to convert to default (USD)
    assertQ(
        req(
            "fl", "id,func:currency($f)",
            "f", fieldName,
            "q", "id:10"),
        "//*[@numFound='1']",
        "//doc/float[@name='func' and .=10]");
    assertQ(
        req(
            "fl", "id,func:currency($f)",
            "f", fieldName,
            "q", "id:5"),
        "//*[@numFound='1']",
        "//doc/float[@name='func' and .=5]");
    assertQ(
        req("fl", "id,score" + fieldName, "f", fieldName, "q", "{!frange u=5}currency($f)"),
        "//*[@numFound='5']",
        "//str[@name='id']='1'",
        "//str[@name='id']='2'",
        "//str[@name='id']='3'",
        "//str[@name='id']='4'",
        "//str[@name='id']='5'");
    assertQ(
        req("fl", "id,score" + fieldName, "f", fieldName, "q", "{!frange l=5 u=10}currency($f)"),
        "//*[@numFound='6']",
        "//str[@name='id']='5'",
        "//str[@name='id']='6'",
        "//str[@name='id']='7'",
        "//str[@name='id']='8'",
        "//str[@name='id']='9'",
        "//str[@name='id']='10'");

    // use the currency function to convert to MXN
    assertQ(
        req(
            "fl", "id,func:currency($f,MXN)",
            "f", fieldName,
            "q", "id:5"),
        "//*[@numFound='1']",
        "//doc/float[@name='func' and .=10]");
    assertQ(
        req(
            "fl", "id,func:currency($f,MXN)",
            "f", fieldName,
            "q", "id:10"),
        "//*[@numFound='1']",
        "//doc/float[@name='func' and .=20]");
    assertQ(
        req("fl", "*,score," + fieldName, "f", fieldName, "q", "{!frange u=10}currency($f,MXN)"),
        "//*[@numFound='5']",
        "//str[@name='id']='1'",
        "//str[@name='id']='2'",
        "//str[@name='id']='3'",
        "//str[@name='id']='4'",
        "//str[@name='id']='5'");
    assertQ(
        req(
            "fl",
            "*,score," + fieldName,
            "f",
            fieldName,
            "q",
            "{!frange l=10 u=20}currency($f,MXN)"),
        "//*[@numFound='6']",
        "//str[@name='id']='5'",
        "//str[@name='id']='6'",
        "//str[@name='id']='7'",
        "//str[@name='id']='8'",
        "//str[@name='id']='9'",
        "//str[@name='id']='10'");
  }

  @Test
  public void testStringValue() {
    assertEquals("3.14,USD", new CurrencyValue(314, "USD").strValue());
    assertEquals("-3.14,GBP", new CurrencyValue(-314, "GBP").strValue());
    assertEquals("3.14,GBP", new CurrencyValue(314, "GBP").strValue());

    CurrencyValue currencyValue = new CurrencyValue(314, "XYZ");
    expectThrows(SolrException.class, currencyValue::strValue);
  }

  @Test
  public void testRangeFacet() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider "
            + "because it exercises the asymmetric exchange rates option it supports",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    clearIndex();

    // NOTE: in our test conversions EUR uses an asymmetric exchange rate
    // these are the equivalent values when converting to:     USD        EUR        GBP
    assertU(adoc("id", "" + 1, fieldName, "10.00,USD")); // 10.00,USD  25.00,EUR   5.00,GBP
    assertU(adoc("id", "" + 2, fieldName, "15.00,EUR")); //  7.50,USD  15.00,EUR   7.50,GBP
    assertU(adoc("id", "" + 3, fieldName, "6.00,GBP")); // 12.00,USD  12.00,EUR   6.00,GBP
    assertU(adoc("id", "" + 4, fieldName, "7.00,EUR")); //  3.50,USD   7.00,EUR   3.50,GBP
    assertU(adoc("id", "" + 5, fieldName, "2,GBP")); //  4.00,USD   4.00,EUR   2.00,GBP
    assertU(commit());

    for (String suffix : Arrays.asList("", ",USD")) {
      assertQ(
          "Ensure that we get correct facet counts back in USD (explicit or implicit default) (facet.range)",
          req(
              "fl",
              "*,score",
              "q",
              "*:*",
              "rows",
              "0",
              "facet",
              "true",
              "facet.range",
              fieldName,
              "f." + fieldName + ".facet.range.start",
              "4.00" + suffix,
              "f." + fieldName + ".facet.range.end",
              "11.00" + suffix,
              "f." + fieldName + ".facet.range.gap",
              "1.00" + suffix,
              "f." + fieldName + ".facet.range.other",
              "all"),
          "count(//lst[@name='counts']/int)=7",
          "//lst[@name='counts']/int[@name='4.00,USD']='1'",
          "//lst[@name='counts']/int[@name='5.00,USD']='0'",
          "//lst[@name='counts']/int[@name='6.00,USD']='0'",
          "//lst[@name='counts']/int[@name='7.00,USD']='1'",
          "//lst[@name='counts']/int[@name='8.00,USD']='0'",
          "//lst[@name='counts']/int[@name='9.00,USD']='0'",
          "//lst[@name='counts']/int[@name='10.00,USD']='1'",
          "//int[@name='after']='1'",
          "//int[@name='before']='1'",
          "//int[@name='between']='3'");
      assertQ(
          "Ensure that we get correct facet counts back in USD (explicit or implicit default) (json.facet)",
          req(
              "fl",
              "*,score",
              "q",
              "*:*",
              "rows",
              "0",
              "json.facet",
              "{ xxx : { type:range, field:"
                  + fieldName
                  + ", "
                  + "          start:'4.00"
                  + suffix
                  + "', gap:'1.00"
                  + suffix
                  + "', end:'11.00"
                  + suffix
                  + "', other:all } }"),
          "count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='5.00,USD']]",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='6.00,USD']]",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='8.00,USD']]",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='9.00,USD']]",
          "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]",
          "//lst[@name='xxx']/lst[@name='before' ]/long[@name='count'][.='1']",
          "//lst[@name='xxx']/lst[@name='after'  ]/long[@name='count'][.='1']",
          "//lst[@name='xxx']/lst[@name='between']/long[@name='count'][.='3']");
    }

    assertQ(
        "Zero value as start range point + mincount (facet.range)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "facet",
            "true",
            "facet.mincount",
            "1",
            "facet.range",
            fieldName,
            "f." + fieldName + ".facet.range.start",
            "0,USD",
            "f." + fieldName + ".facet.range.end",
            "11.00,USD",
            "f." + fieldName + ".facet.range.gap",
            "1.00,USD",
            "f." + fieldName + ".facet.range.other",
            "all"),
        "count(//lst[@name='counts']/int)=4",
        "//lst[@name='counts']/int[@name='3.00,USD']='1'",
        "//lst[@name='counts']/int[@name='4.00,USD']='1'",
        "//lst[@name='counts']/int[@name='7.00,USD']='1'",
        "//lst[@name='counts']/int[@name='10.00,USD']='1'",
        "//int[@name='before']='0'",
        "//int[@name='after']='1'",
        "//int[@name='between']='4'");
    assertQ(
        "Zero value as start range point + mincount (json.facet)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "json.facet",
            "{ xxx : { type:range, mincount:1, field:"
                + fieldName
                + ", start:'0.00,USD', gap:'1.00,USD', end:'11.00,USD', other:all } }"),
        "count(//lst[@name='xxx']/arr[@name='buckets']/lst)=4",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='3.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]",
        "//lst[@name='xxx']/lst[@name='before' ]/long[@name='count'][.='0']",
        "//lst[@name='xxx']/lst[@name='after'  ]/long[@name='count'][.='1']",
        "//lst[@name='xxx']/lst[@name='between']/long[@name='count'][.='4']");

    // NOTE: because of asymmetric EUR exchange rate, these buckets are diff from the similar
    // looking
    // USD based request above
    // This request converts the values in each doc into EUR to decide what range buck it's in.
    assertQ(
        "Ensure that we get correct facet counts back in EUR (facet.range)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "facet",
            "true",
            "facet.range",
            fieldName,
            "f." + fieldName + ".facet.range.start",
            "8.00,EUR",
            "f." + fieldName + ".facet.range.end",
            "22.00,EUR",
            "f." + fieldName + ".facet.range.gap",
            "2.00,EUR",
            "f." + fieldName + ".facet.range.other",
            "all"),
        "count(//lst[@name='counts']/int)=7",
        "//lst[@name='counts']/int[@name='8.00,EUR']='0'",
        "//lst[@name='counts']/int[@name='10.00,EUR']='0'",
        "//lst[@name='counts']/int[@name='12.00,EUR']='1'",
        "//lst[@name='counts']/int[@name='14.00,EUR']='1'",
        "//lst[@name='counts']/int[@name='16.00,EUR']='0'",
        "//lst[@name='counts']/int[@name='18.00,EUR']='0'",
        "//lst[@name='counts']/int[@name='20.00,EUR']='0'",
        "//int[@name='before']='2'",
        "//int[@name='after']='1'",
        "//int[@name='between']='2'");
    assertQ(
        "Ensure that we get correct facet counts back in EUR (json.facet)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "json.facet",
            "{ xxx : { type:range, field:"
                + fieldName
                + ", start:'8.00,EUR', gap:'2.00,EUR', end:'22.00,EUR', other:all } }"),
        "count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='8.00,EUR']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='10.00,EUR']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='12.00,EUR']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='14.00,EUR']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='16.00,EUR']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='18.00,EUR']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='20.00,EUR']]",
        "//lst[@name='xxx']/lst[@name='before' ]/long[@name='count'][.='2']",
        "//lst[@name='xxx']/lst[@name='after'  ]/long[@name='count'][.='1']",
        "//lst[@name='xxx']/lst[@name='between']/long[@name='count'][.='2']");

    // GBP has a symmetric exchange rate with USD, so these counts are *similar* to the USD based
    // request above... but the asymmetric EUR/USD rate means that when computing counts relative to
    // GBP the EUR based docs wind up in diff buckets
    assertQ(
        "Ensure that we get correct facet counts back in GBP (facet.range)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "facet",
            "true",
            "facet.range",
            fieldName,
            "f." + fieldName + ".facet.range.start",
            "2.00,GBP",
            "f." + fieldName + ".facet.range.end",
            "5.50,GBP",
            "f." + fieldName + ".facet.range.gap",
            "0.50,GBP",
            "f." + fieldName + ".facet.range.other",
            "all"),
        "count(//lst[@name='counts']/int)=7",
        "//lst[@name='counts']/int[@name='2.00,GBP']='1'",
        "//lst[@name='counts']/int[@name='2.50,GBP']='0'",
        "//lst[@name='counts']/int[@name='3.00,GBP']='0'",
        "//lst[@name='counts']/int[@name='3.50,GBP']='1'",
        "//lst[@name='counts']/int[@name='4.00,GBP']='0'",
        "//lst[@name='counts']/int[@name='4.50,GBP']='0'",
        "//lst[@name='counts']/int[@name='5.00,GBP']='1'",
        "//int[@name='before']='0'",
        "//int[@name='after']='2'",
        "//int[@name='between']='3'");
    assertQ(
        "Ensure that we get correct facet counts back in GBP (json.facet)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "json.facet",
            "{ xxx : { type:range, field:"
                + fieldName
                + ", start:'2.00,GBP', gap:'0.50,GBP', end:'5.50,GBP', other:all } }"),
        "count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='2.00,GBP']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='2.50,GBP']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='3.00,GBP']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='3.50,GBP']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='4.00,GBP']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='4.50,GBP']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='5.00,GBP']]",
        "//lst[@name='xxx']/lst[@name='before' ]/long[@name='count'][.='0']",
        "//lst[@name='xxx']/lst[@name='after'  ]/long[@name='count'][.='2']",
        "//lst[@name='xxx']/lst[@name='between']/long[@name='count'][.='3']");

    assertQ(
        "Ensure that we can set a gap in a currency other than the start and end currencies (facet.range)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "facet",
            "true",
            "facet.range",
            fieldName,
            "f." + fieldName + ".facet.range.start",
            "4.00,USD",
            "f." + fieldName + ".facet.range.end",
            "11.00,USD",
            "f." + fieldName + ".facet.range.gap",
            "0.50,GBP",
            "f." + fieldName + ".facet.range.other",
            "all"),
        "count(//lst[@name='counts']/int)=7",
        "//lst[@name='counts']/int[@name='4.00,USD']='1'",
        "//lst[@name='counts']/int[@name='5.00,USD']='0'",
        "//lst[@name='counts']/int[@name='6.00,USD']='0'",
        "//lst[@name='counts']/int[@name='7.00,USD']='1'",
        "//lst[@name='counts']/int[@name='8.00,USD']='0'",
        "//lst[@name='counts']/int[@name='9.00,USD']='0'",
        "//lst[@name='counts']/int[@name='10.00,USD']='1'",
        "//int[@name='before']='1'",
        "//int[@name='after']='1'",
        "//int[@name='between']='3'");
    assertQ(
        "Ensure that we can set a gap in a currency other than the start and end currencies (json.facet)",
        req(
            "fl",
            "*,score",
            "q",
            "*:*",
            "rows",
            "0",
            "json.facet",
            "{ xxx : { type:range, field:"
                + fieldName
                + ", start:'4.00,USD', gap:'0.50,GBP', end:'11.00,USD', other:all } }"),
        "count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='5.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='6.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='8.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='0']][str[@name='val'][.='9.00,USD']]",
        "//lst[@name='xxx']/arr[@name='buckets']/lst[long[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]",
        "//lst[@name='xxx']/lst[@name='before' ]/long[@name='count'][.='1']",
        "//lst[@name='xxx']/lst[@name='after'  ]/long[@name='count'][.='1']",
        "//lst[@name='xxx']/lst[@name='between']/long[@name='count'][.='3']");

    for (SolrParams facet :
        Arrays.asList(
            params(
                "facet",
                "true",
                "facet.range",
                fieldName,
                "f." + fieldName + ".facet.range.start",
                "4.00,USD",
                "f." + fieldName + ".facet.range.end",
                "11.00,EUR",
                "f." + fieldName + ".facet.range.gap",
                "1.00,USD",
                "f." + fieldName + ".facet.range.other",
                "all"),
            params(
                "json.facet",
                "{ xxx : { type:range, field:"
                    + fieldName
                    + ", start:'4.00,USD', "
                    + "          gap:'1.00,USD', end:'11.00,EUR', other:all } }"))) {
      assertQEx(
          "Ensure that we throw an error if we try to use different start and end currencies",
          "Cannot compare CurrencyValues when their currencies are not equal",
          req(facet, "q", "*:*"),
          SolrException.ErrorCode.BAD_REQUEST);
    }
  }

  @Test
  public void testMockFieldType() {
    assumeTrue(
        "This test is only applicable to the mock exchange rate provider",
        expectedProviderClass.equals(MockExchangeRateProvider.class));

    clearIndex();

    assertU(adoc("id", "1", fieldName, "1.00,USD"));
    assertU(adoc("id", "2", fieldName, "1.00,EUR"));
    assertU(adoc("id", "3", fieldName, "1.00,NOK"));
    assertU(commit());

    assertQ(
        req("fl", "*,score", "q", fieldName + ":5.0,NOK"),
        "//*[@numFound='1']",
        "//str[@name='id']='1'");
    assertQ(
        req("fl", "*,score", "q", fieldName + ":1.2,USD"),
        "//*[@numFound='1']",
        "//str[@name='id']='2'");
    assertQ(
        req("fl", "*,score", "q", fieldName + ":0.2,USD"),
        "//*[@numFound='1']",
        "//str[@name='id']='3'");
    assertQ(req("fl", "*,score", "q", fieldName + ":99,USD"), "//*[@numFound='0']");
  }

  @Test
  public void testAsymmetricPointQuery() {
    assumeTrue(
        "This test is only applicable to the XML file based exchange rate provider",
        expectedProviderClass.equals(FileExchangeRateProvider.class));

    clearIndex();
    assertU(adoc("id", "" + 1, fieldName, "10.00,USD"));
    assertU(adoc("id", "" + 2, fieldName, "15.00,EUR"));
    assertU(commit());

    assertQ(req("fl", "*,score", "q", fieldName + ":15.00,EUR"), "//str[@name='id']='2'");
    assertQ(req("fl", "*,score", "q", fieldName + ":7.50,USD"), "//str[@name='id']='2'");
    assertQ(req("fl", "*,score", "q", fieldName + ":7.49,USD"), "//*[@numFound='0']");
    assertQ(req("fl", "*,score", "q", fieldName + ":7.51,USD"), "//*[@numFound='0']");
  }
}
